namespace Profit
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Xml.Linq;
    using System.Xml.Schema;
    using Core;
    using Core.Base;
    using Core.Contracts.Enums;
    using Core.Contracts.Interfaces;
    using Services;

    /// <summary>
    /// Extension methods class
    /// </summary>
    internal static class ExtensionMethods
    {
        private const string REGEX_FITTING = @"(?:\[)(?<robot>.*)\,(?<description>.*)(?:\])";

        private static XmlSchema _agentSchema = null;
        private static XmlSchemaSet _schemaSet = null;
        /// <summary>
        /// Obtains the <see cref="T:System.Xml.Schema.XmlSchema"/> single instance containing the agents backup xsd or null if none exists.
        /// </summary>
        private static XmlSchema AgentSchemaValidation
        {
            get
            {
                if (_agentSchema == null)
                {
                    var stream = Assembly.GetEntryAssembly().GetManifestResourceStream("Profit.Resources.agents.xsd");
                    _agentSchema = stream == null ? null : XmlSchema.Read(stream, null);
                }
                return _agentSchema;
            }
        }
        /// <summary>
        /// Obtains the <see cref="T:System.Xml.Schema.XmlSchemaSet"/> single instance used to validate xml files
        /// </summary>
        private static XmlSchemaSet SchemaSet
        {
            get
            {
                if (_schemaSet == null)
                {
                    _schemaSet = new XmlSchemaSet();

                    if (AgentSchemaValidation != null)
                        _schemaSet.Add(AgentSchemaValidation);

                    if (_schemaSet.Count > 0)
                        _schemaSet.Compile();
                }
                return _schemaSet;
            }
        }
        /// <summary>
        /// Loads agents from a file
        /// </summary>
        /// <param name="agents"></param>
        /// <param name="filename"></param>
        internal static void Load(this IList<IAgent> agents, string filename)
        {
            if (!File.Exists(filename) || agents == null) return;

            var doc = XDocument.Load(filename);
            try
            {
                doc.Validate(SchemaSet, null, false);
            }
            catch (XmlSchemaValidationException)
            {
                Trace.TraceError("Agent file \"{0}\" format is not valid.", Path.GetFullPath(filename));
                return;
            }

            foreach (var element in doc.Elements("agents").Elements("agent"))
            {
                if (!element.HasAttributes || element.Attribute("name") == null || string.IsNullOrEmpty(element.Attribute("name").Value))
                    continue;

                var agent = Agent.CreateNew(element.Attribute("name").Value);
                if (agent == null) return;

                agent.IsNotifying = false;

                var img = element.Elements("portrait").FirstOrDefault();
                if (img != null)
                {
                    agent.ImageSource = string.IsNullOrEmpty(img.Value) ? null : new Uri(img.Value);

                    // Checks if the image source is a file and exists
                    if (agent.ImageSource != null && agent.ImageSource.IsFile && !File.Exists(agent.ImageSource.LocalPath))
                        agent.ImageSource = null;
                }

                foreach (var extension in element.Elements("extensions").Elements())
                {
                    var current = extension;
                    var ext = agent.Extensions.Where(e => e.GetType().Name == current.Name).FirstOrDefault();
                    if (ext != null)
                        ext.Level = ushort.Parse(current.Value);
                }

                agent.IsNotifying = true;
                if (!agents.Contains(agent))
                    agents.Add(agent);
            }
        }
        /// <summary>
        /// Saves all agents to a file
        /// </summary>
        /// <param name="agents"></param>
        /// <param name="filename"></param>
        internal static void Save(this IEnumerable<IAgent> agents, string filename)
        {
            var sb = new StringBuilder();
            sb.Append("<?xml version=\"1.0\"?><agents>");

            foreach (var agent in agents)
                agent.ToXmlString(sb);

            sb.Append("</agents>");

            try
            {
                File.WriteAllText(filename, sb.ToString());
                Trace.TraceInformation("All agents sucessfully saved to {0}", Path.GetFullPath(filename));
            }
            catch (Exception exp)
            {
                Trace.TraceError("An exception of type {0} occured while attempting to save agents to \"{1}\". Details: {2}", exp.GetType(), Path.GetFullPath(filename), exp.Message);
            }
        }
        /// <summary>
        /// Load a collection of fittings from a file
        /// </summary>
        /// <param name="fittings"></param>
        /// <param name="filename"></param>
        internal static void Load(this IList<IFitting> fittings, string filename)
        {
            if (!File.Exists(filename) || fittings == null) return;

            // TODO : XSD validation
            using (var reader = new StreamReader(filename, Encoding.UTF8))
            {
                var elements = XElement.Load(reader);

                foreach (var element in elements.Elements("fitting"))
                {
                    var currentFitting = element;

                    if (!currentFitting.HasAttributes ||
                        currentFitting.Attribute("name") == null ||
                        string.IsNullOrEmpty(currentFitting.Attribute("name").Value))
                        continue;

                    var robotElement = currentFitting.Elements("robot").FirstOrDefault();
                    if (robotElement == null ||
                        !robotElement.HasAttributes ||
                        robotElement.Attribute("name") == null ||
                        string.IsNullOrEmpty(robotElement.Attribute("name").Value))
                        continue;

                    var bot = Repository.Default.LoadByName<IRobot>(robotElement.Attribute("name").Value);
                    if (bot == null) continue;

                    var fitting = new Fitting(currentFitting.Attribute("name").Value, bot);
                    if (fitting.Robot == null) continue;

                    fitting.IsNotifying = false;
                    fitting.Robot.IsNotifying = false;

                    foreach (var el in robotElement.Elements("head"))
                        fitting.Robot.AddModule(Repository.GetModule(el.Value), false);

                    foreach (var el in robotElement.Elements("chassis"))
                        fitting.Robot.AddModule(Repository.GetModule(el.Value), false);

                    foreach (var el in robotElement.Elements("legs"))
                        fitting.Robot.AddModule(Repository.GetModule(el.Value), false);

                    fitting.IsNotifying = true;
                    fitting.Robot.IsNotifying = true;

                    if (!fittings.Contains(fitting))
                        fittings.Add(fitting);
                }
                reader.Close();
            }

        }
        /// <summary>
        /// Loads all ammunitions types usable by the module
        /// </summary>
        /// <param name="module">An IModule instance</param>
        internal static void LoadUsableAmmunitions(this IModule module)
        {
            if (module == null || !module.CanHaveAmmunitions || module.UsableAmmunitions != null) return;

            var ammoType = module.Features.Where(f => f.FeatureType == FeaturesEnum.AmmoType).FirstOrDefault();
            if (ammoType == null) return;

            var ammos = Repository.Default.Ammunitions.Where(a => a.Type == (AmmunitionTypesEnum)ammoType.Value);
            if (!ammos.Any()) return;

            module.UsableAmmunitions =
                new ObservableCollection<IAmmunition>(new[] { NullAmmunition.Instance }.Union(ammos.Select(a => (IAmmunition)a.Clone())));
        }
        /// <summary>
        /// Save all fittings to a file
        /// </summary>
        /// <param name="fittings"></param>
        /// <param name="filename"></param>
        internal static void Save(this IEnumerable<IFitting> fittings, string filename)
        {
            var xmlExporter = new XmlFittingExporter();

            var sb = new StringBuilder();
            sb.Append("<?xml version=\"1.0\"?><fittings>");
            foreach (var fitting in fittings)
            {
                xmlExporter.Export(fitting, sb);
                fitting.IsDirty = false;
            }
            sb.Append("</fittings>");
            try
            {
                File.WriteAllText(filename, sb.ToString());
                Trace.TraceInformation("All fittings sucessfully saved to {0}", Path.GetFullPath(filename));
            }
            catch (Exception exp)
            {
                Trace.TraceError("An exception of type {0} occured while attempting to save fittings to \"{1}\". Details: {2}", exp.GetType(), Path.GetFullPath(filename), exp.Message);
            }
        }
        /// <summary>
        /// Determines if a fitting name already exists
        /// </summary>
        /// <param name="fittings">An IEnumerable collection of IFitting</param>
        /// <param name="name">The non case sensitive fitting name to test</param>
        /// <returns>True if the name exists otherwise false</returns>
        internal static bool Exists(this IEnumerable<IFitting> fittings, string name)
        {
            return fittings
                .Where(f => String.Equals(f.Name, name, StringComparison.InvariantCultureIgnoreCase))
                .Any();
        }
        /// <summary>
        /// Obtains the fitting name of a fitting strored as a text
        /// </summary>
        /// <param name="value"></param>
        /// <returns>The fitting name or null</returns>
        internal static string GetFittingName(this string value)
        {
            if (string.IsNullOrEmpty(value) || !value.IsFittingTextFormat()) return null;
            var match = Regex.Match(value, REGEX_FITTING);
            return match.Groups["description"].Success ? match.Groups["description"].Value.Trim() : null;
        }
        /// <summary>
        /// Obtains the robot name of a fitting strored as a text
        /// </summary>
        /// <param name="value"></param>
        /// <returns>The robot name or null</returns>
        internal static string GetFittingRobotName(this string value)
        {
            if (string.IsNullOrEmpty(value) || !value.IsFittingTextFormat()) return null;
            var match = Regex.Match(value, REGEX_FITTING);
            return match.Groups["robot"].Success ? match.Groups["robot"].Value : null;
        }
        /// <summary>
        /// Parse a text formatted fitting to an instance of IFitting
        /// </summary>
        /// <param name="value">The text formatted fitting string</param>
        /// <returns>An instance of IFitting or null</returns>
        internal static IFitting ToFitting(this string value)
        {
            if (string.IsNullOrEmpty(value) || !value.IsFittingTextFormat()) return null;

            var match = Regex.Match(value, REGEX_FITTING);
            if (!match.Success) return null;

            string robotName = match.Groups["robot"].Success ? match.Groups["robot"].Value : null;
            string fittingDescription = match.Groups["description"].Success ? match.Groups["description"].Value.Trim() : null;

            if (string.IsNullOrEmpty(robotName)) return null;

            var bot = Repository.Default.LoadByName<IRobot>(robotName);
            if (bot == null) return null;

            var fitting = new Fitting(string.IsNullOrEmpty(fittingDescription) ? bot.Name : fittingDescription, bot);

            var parts = value.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length > 0)
            {
                for (int i = 1; i < parts.Length; i++) // skip first line already parsed
                {
                    if (string.IsNullOrEmpty(parts[i])) continue;

                    IModule module = Repository.GetModule(parts[i]);
                    if (module == null) continue;

                    fitting.Robot.AddModule(module, false);
                }
            }

            return fitting;
        }
        /// <summary>
        /// Determines if the string value is a fitting text format
        /// </summary>
        /// <param name="value">The string value to check</param>
        /// <returns>True if the format is correct otherwise false</returns>
        internal static bool IsFittingTextFormat(this string value)
        {
            return Regex.IsMatch(value, REGEX_FITTING);
        }
        /// <summary>
        /// Creates an Xml representation of an agent
        /// </summary>
        /// <param name="agent">The IAgent instance to use</param>
        /// <param name="stringBuilder">The StringBuilder instance to use</param>
        internal static void ToXmlString(this IAgent agent, StringBuilder stringBuilder)
        {
            if (agent == null || stringBuilder == null) return;

            stringBuilder.AppendFormat("<agent name=\"{0}\"><portrait>{1}</portrait>", agent.Name, agent.ImageSource);
           
            stringBuilder.Append("<extensions>");
            foreach (var extension in agent.Extensions.Where(e => e.Level > 0))
            {
                stringBuilder.AppendFormat("<{0}>{1}</{0}>", extension.GetType().Name, extension.Level);
            }
            stringBuilder.Append("</extensions></agent>");
        }
        /// <summary>
        /// Throws a ObjectDisposedException exception if the instance has been disposed
        /// </summary>
        /// <param name="disposable"></param>
        internal static void ThrowIfDisposed(this IIsDisposed disposable)
        {
            if (disposable.IsDisposed)
                throw new ObjectDisposedException("The instance has been disposed");
        }
    }
}